package pl.enigmatic.math;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import pl.enigmatic.ValueConverter;


/**
 * Represents the concept of a mathematical bijection mapping.
 * @author reggie_7 *
 * @param <X> the left values type for the mapping
 * @param <Y> the right values type for the mapping
 */
public class BijectionMap<X, Y> implements ValueConverter<X, Y> {

	/** the mapping <code>X</code> &rarr; <code>Y</code> */
	private final HashMap<X, Y> left = new HashMap<X, Y>();
	/** a converter to use for all incoming <code>X</code> values */
	private ValueConverter<X, X> leftConverter = new ValueConverter.IdentityImpl<X>();
	/** the mapping <code>Y</code> &rarr; <code>X</code> */
	private final HashMap<Y, X> right = new HashMap<Y, X>();
	/** a converter to use for all incoming <code>Y</code> values */
	private ValueConverter<Y, Y> rightConverter = new ValueConverter.IdentityImpl<Y>();

	/** Removes all of the mappings from <code>this</code> bijection map. The map will be empty after this call returns. */
	public void clear() {
		left.clear();
		right.clear();
	}

	/**
	 * Returns <code>true</code> if <code>this</code> map contains an <code>X</code> &rarr; <code>Y</code> mapping for the specified key
	 * <code>x</code>.
	 * @param x the key whose presence in <code>this</code> map is to be tested
	 * @return <code>true</code> if <code>this</code> map contains an <code>X</code> &rarr; <code>Y</code> mapping for the specified key
	 */
	public boolean containsX(final X x) {
		return left.containsKey(leftConverter.convert(x));
	}

	/**
	 * Returns <code>true</code> if <code>this</code> map contains an <code>Y</code> &rarr; <code>X</code> mapping for the specified key
	 * <code>y</code>.
	 * @param y the key whose presence in <code>this</code> map is to be tested
	 * @return <code>true</code> if <code>this</code> map contains an <code>Y</code> &rarr; <code>X</code> mapping for the specified key
	 */
	public boolean containsY(final Y y) {
		return right.containsKey(rightConverter.convert(y));
	}

	/**
	 * Returns a {@link Set} view of the <code>X</code> &rarr; <code>Y</code> mappings contained in this map. This call should be used only to allow
	 * iteration over pairs. Changing the result set itself may cause inconsistency of data.
	 * @return a set view of the <code>X</code> &rarr; <code>Y</code> mappings contained in <code>this</code> map
	 */
	public Set<Map.Entry<X, Y>> entrySetX() {
		return left.entrySet();
	}

	/**
	 * Returns a {@link Set} view of the <code>Y</code> &rarr; <code>X</code> mappings contained in this map. This call should be used only to allow
	 * iteration over pairs. Changing the result set itself may cause inconsistency of data.
	 * @return a set view of the <code>Y</code> &rarr; <code>X</code> mappings contained in this map
	 */
	public Set<Map.Entry<Y, X>> entrySetY() {
		return right.entrySet();
	}

	/**
	 * <p>
	 * Returns the <code>Y</code> value to which the specified <code>x</code> key is mapped, or <code>null</code> if this map contains no mapping for
	 * the key.
	 * </p>
	 * <p>
	 * More formally, if this map contains a mapping from a key <code>k</code> to a value <code>v</code> such that
	 * <code>(x==null ? k==null : x.equals(k))</code>, then this method returns <code>v</code>; otherwise it returns <code>null</code>. (There can be
	 * at most one such mapping.)
	 * </p>
	 * 
	 * <p>
	 * A return value of <code>null</code> does not necessarily indicate that the map contains no mapping for the key; it's also possible that the map
	 * explicitly maps the key to <code>null</code>. The {@link BijectionMap#containsX(Object)} operation may be used to distinguish these two cases.
	 * </p>
	 * 
	 * @param x the key whose associated value is to be returned
	 * @return the <code>Y</code> value to which the specified key is mapped, or <code>null</code> if this map contains no mapping for the key
	 */
	public Y getY(final X x) {
		return left.get(leftConverter.convert(x));
	}

	/**
	 * <p>
	 * Returns the <code>X</code> value to which the specified <code>y</code> key is mapped, or <code>null</code> if this map contains no mapping for
	 * the key.
	 * </p>
	 * <p>
	 * More formally, if this map contains a mapping from a key <code>k</code> to a value <code>v</code> such that
	 * <code>(y==null ? k==null : y.equals(k))</code>, then this method returns <code>v</code>; otherwise it returns <code>null</code>. (There can be
	 * at most one such mapping.)
	 * </p>
	 * 
	 * <p>
	 * A return value of <code>null</code> does not necessarily indicate that the map contains no mapping for the key; it's also possible that the map
	 * explicitly maps the key to <code>null</code>. The {@link BijectionMap#containsY(Object)} operation may be used to distinguish these two cases.
	 * </p>
	 * 
	 * @param y the key whose associated value is to be returned
	 * @return the <code>X</code> value to which the specified key is mapped, or <code>null</code> if this map contains no mapping for the key
	 */
	public X getX(final Y y) {
		return right.get(rightConverter.convert(y));
	}

	/**
	 * Returns <code>true</code> if this map contains no key-value mappings.
	 * @return <code>true</code> if <code>this</code> map contains no key-value
	 */
	public boolean isEmpty() {
		return left.isEmpty();
	}

	/**
	 * Returns a {@link Set} view of the <code>X</code> keys contained in <code>this</code> map. This call should be used only to allow iteration over
	 * the keys. Changing the result set itself may cause inconsistency of data.
	 * @return a set view of the <code>X</code> keys contained in this map
	 */
	public Set<X> setX() {
		return left.keySet();
	}

	/**
	 * Returns a {@link Set} view of the <code>Y</code> keys contained in <code>this</code> map. This call should be used only to allow iteration over
	 * the keys. Changing the result set itself may cause inconsistency of data.
	 * @return a set view of the <code>Y</code> keys contained in this map
	 */
	public Set<Y> setY() {
		return right.keySet();
	}

	/**
	 * Returns the number of key-value mappings in this map.
	 * @return the number of key-value mappings in this map
	 */
	public int size() {
		return left.size();
	}

	/**
	 * Removes the mapping for the specified <code>x</code> key from <code>this</code> map if present.
	 * @param x key whose mapping is to be removed from the map
	 * @return the previous value associated with <code>x</code>, or <code>null</code> if there was no mapping for the key. (A <code>null</code>
	 *         return can also indicate that the map previously associated <code>null</code> with <code>x</code>.)
	 */
	public Y removeX(final X x) {
		if (containsX(x)) {
			final Y y = left.remove(leftConverter.convert(x));
			right.remove(y);
			return y;
		}
		return null;
	}

	/**
	 * Removes the mapping for the specified <code>y</code> key from <code>this</code> map if present.
	 * @param y key whose mapping is to be removed from the map
	 * @return the previous value associated with <code>y</code>, or <code>null</code> if there was no mapping for the key. (A <code>null</code>
	 *         return can also indicate that the map previously associated <code>null</code> with <code>y</code>.)
	 */
	public X removeY(final Y y) {
		if (containsY(y)) {
			final X x = right.remove(rightConverter.convert(y));
			left.remove(x);
			return x;
		}
		return null;
	}

	/**
	 * Associates the specified <code>y</code> value with the specified <code>x</code> key in <code>this</code> map. If the map previously contained a
	 * mapping for the key, the old value is replaced.
	 * @param x key with which the specified value is to be associated
	 * @param y value to be associated with the specified key
	 * @return the previous value associated with <code>x</code>, or <code>null</code> if there was no mapping for the key. (A <code>null</code>
	 *         return can also indicate that the map previously associated <code>null</code> with <code>x</code>.)
	 */
	public Y putXY(X x, Y y) {
		x = leftConverter.convert(x);
		y = rightConverter.convert(y);
		final Y res = removeX(x);
		removeY(y);
		left.put(x, y);
		right.put(y, x);
		return res;
	}

	/**
	 * Associates the specified <code>x</code> value with the specified <code>y</code> key in <code>this</code> map. If the map previously contained a
	 * mapping for the key, the old value is replaced.
	 * @param y key with which the specified value is to be associated
	 * @param x value to be associated with the specified key
	 * @return the previous value associated with <code>y</code>, or <code>null</code> if there was no mapping for the key. (A <code>null</code>
	 *         return can also indicate that the map previously associated <code>null</code> with <code>y</code>.)
	 */
	public X putYX(Y y, X x) {
		x = leftConverter.convert(x);
		y = rightConverter.convert(y);
		removeX(x);
		final X res = removeY(y);
		left.put(x, y);
		right.put(y, x);
		return res;
	}

	/**
	 * Copies all of the mappings from the specified <code>map</code> to <code>this</code> map. These mappings will replace any mappings that
	 * <code>this</code> map had for any of the keys currently in the specified <code>map</code>. If the <code>map</code> is not a bijection the
	 * result is not specified clearly.
	 * @param map mappings to be stored in this map
	 * @throws NullPointerException if the specified <code>map</code> is <code>null</code>
	 */
	public void putAllXY(final Map<? extends X, ? extends Y> map) {
		for (final Entry<? extends X, ? extends Y> e : map.entrySet()) {
			putXY(e.getKey(), e.getValue());
		}
	}

	/**
	 * Copies all of the mappings from the specified <code>map</code> to <code>this</code> map. These mappings will replace any mappings that
	 * <code>this</code> map had for any of the keys currently in the specified <code>map</code>. If the <code>map</code> is not a bijection the
	 * result is not specified clearly.
	 * @param map mappings to be stored in this map
	 * @throws NullPointerException if the specified <code>map</code> is <code>null</code>
	 */
	public void putAllYX(final Map<? extends Y, ? extends X> map) {
		for (final Entry<? extends Y, ? extends X> e : map.entrySet()) {
			putYX(e.getKey(), e.getValue());
		}
	}

	/**
	 * Copies all of the mappings from the specified <code>map</code> to <code>this</code> map. These mappings will replace any mappings that
	 * <code>this</code> map had for any of the keys currently in the specified <code>map</code>.
	 * @param map mappings to be stored in this map
	 * @throws NullPointerException if the specified <code>map</code> is <code>null</code>
	 */
	public void putAllXY(final BijectionMap<? extends X, ? extends Y> map) {
		putAllXY(map.left);
	}

	/**
	 * Copies all of the mappings from the specified <code>map</code> to <code>this</code> map. These mappings will replace any mappings that
	 * <code>this</code> map had for any of the keys currently in the specified <code>map</code>.
	 * @param map mappings to be stored in this map
	 * @throws NullPointerException if the specified <code>map</code> is <code>null</code>
	 */
	public void putAllYX(final BijectionMap<? extends Y, ? extends X> map) {
		putAllYX(map.left);
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + ": " + left;
	}

	/**
	 * Converts the given <code>xs</code> array of <code>X</code> values to their corresponding representation in <code>Y</code> defined by
	 * <code>this</code> map.
	 * @param xs X values to convert to Y
	 * @return a list of converted values
	 */
	public List<Y> convertX(final X... xs) {
		final List<Y> res = new ArrayList<Y>();
		for (final X element : xs) {
			res.add(getY(element));
		}
		return res;
	}

	/**
	 * Converts the given <code>ys</code> array of <code>Y</code> values to their corresponding representation in <code>X</code> defined by
	 * <code>this</code> map.
	 * @param ys Y values to convert to X
	 * @return a list of converted values
	 */
	public List<X> convertY(final Y... ys) {
		final List<X> res = new ArrayList<X>();
		for (final Y element : ys) {
			res.add(getX(element));
		}
		return res;
	}

	/**
	 * Converts the given <code>set</code> of <code>X</code> values to their corresponding representation in <code>Y</code> defined by
	 * <code>this</code> map.
	 * @param set X values to convert to Y
	 * @return a set of converted values
	 */
	public Set<Y> convertX(final Set<X> set) {
		final Set<Y> res = new HashSet<Y>();
		for (final X x : set) {
			res.add(getY(x));
		}
		return res;
	}

	/**
	 * Converts the given <code>set</code> of <code>Y</code> values to their corresponding representation in <code>X</code> defined by
	 * <code>this</code> map.
	 * @param set Y values to convert to X
	 * @return a set of converted values
	 */
	public Set<X> convertY(final Set<Y> set) {
		final Set<X> res = new HashSet<X>();
		for (final Y y : set) {
			res.add(getX(y));
		}
		return res;
	}

	/** @return a converter to use for all incoming <code>X</code> values */
	public ValueConverter<X, X> getLeftConverter() {
		return leftConverter;
	}

	/**
	 * Sets the left (<code>X</code>) converter to the given one. If <code>leftConverter</code> is <code>null</code> - an identity converter will be
	 * used instead.
	 * @param leftConverter the converter to use
	 */
	public void setLeftConverter(ValueConverter<X, X> leftConverter) {
		if (null == leftConverter) {
			leftConverter = new ValueConverter.IdentityImpl<X>();
		}
		this.leftConverter = leftConverter;
	}

	/** @return a converter to use for all incoming <code>Y</code> values */
	public ValueConverter<Y, Y> getRightConverter() {
		return rightConverter;
	}

	/**
	 * Sets the right (<code>Y</code>) converter to the given one. If <code>rightConverter</code> is <code>null</code> - an identity converter will be
	 * used instead.
	 * @param rightConverter the converter to use
	 */
	public void setRightConverter(ValueConverter<Y, Y> rightConverter) {
		if (null == rightConverter) {
			rightConverter = new ValueConverter.IdentityImpl<Y>();
		}
		this.rightConverter = rightConverter;
	}

	/**
	 * Implements {@link ValueConverter#convert(Object)} by simply calling {@link BijectionMap#getY(Object)}.
	 * @see ValueConverter#convert(Object)
	 */
	@Override
	public Y convert(final X value) {
		return getY(value);
	}

	/**
	 * Implements {@link ValueConverter#convertBack(Object)} by simply calling {@link BijectionMap#getX(Object)}.
	 * @see ValueConverter#convertBack(Object)
	 */
	@Override
	public X convertBack(final Y value) {
		return getX(value);
	}
}
